// HttpRequest.java - Class that represents HTTP requests.
//
// Copyright (C) 1999-2002  Smart Software Consulting
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// Smart Software Consulting
// 1688 Silverwood Court
// Danville, CA  94526-3079
// USA
//
// http://www.smartsc.com
//

package com.smartsc.http;

import java.io.BufferedReader;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;

import com.smartsc.util.Base64;
import com.smartsc.util.CaselessHashtable;
import com.smartsc.util.NVPairParser;

public class
HttpRequest
implements HttpStatusCodes, HttpReasonPhrases,
	HttpHeaders, HttpServletRequest
{
	protected
	HttpRequest( HttpServer server, Socket socket)
	throws IOException
	{
		this.server = server;
		this.socket = socket;

		is = new BufferedInputStream(
			socket.getInputStream(), server.getBufferSize() );
	}

	protected
	void
	parseRequest()
	throws HttpException
	{
		try
		{
			// Parse first line
			String s = readLine();
			StringTokenizer st = new StringTokenizer( s);

			method      = st.nextToken();
			verbatimRequestPath = st.nextToken();
			protocol    = st.nextToken();

			parseRequestPath( verbatimRequestPath );

			// Don't accept proxy requests (i.e. "GET http://host/uri ...")
			if( !requestPath.startsWith( "/" )
			||  st.hasMoreTokens() )
			{
				throw new HttpException( SC_BAD_REQUEST, RP_BAD_REQUEST);
			}

			// Parse headers
			s = readLine();
			while( !"".equals( s))
			{
				int idx = s.indexOf( ":");
				if( idx < 0)
				{
					server.log( "Bad Header: '" + s + "'");
					throw new HttpException( SC_BAD_REQUEST, RP_BAD_REQUEST);
				}
				String name  = s.substring( 0, idx);
				String value = "";
				if( idx < s.length()-1)
				{
					value = s.substring( idx+1).trim();
				}
				setHeader( name, value);

				s = readLine();
			}

			// Look for authorization
			String auth = getHeader( AUTHORIZATION);
			if( auth != null)
			{
				st = new StringTokenizer( auth);
				if( st.hasMoreTokens())
					authType = st.nextToken();
				if( st.hasMoreTokens())
				{
					String credentials = Base64.decode( st.nextToken());
					int colon = credentials.indexOf( ':');
					if( colon != -1)
						remoteUser = credentials.substring( 0, colon);
				}
			}

			// Look for session
			sessionName = server.getSessionName();
			Cookie sessionCookie = getCookie( sessionName);
			if( sessionCookie != null)
			{
				reqSessionId = sessionCookie.getValue();
				if( reqSessionId != null)
				{
					reqSessionIdFromCookie = true;
				}
			}
			else
			{
				// Can't call getParameter( sessionName ) because
				// that would/could cause POST data to be parsed!
				Vector v = (Vector)parameters.get( sessionName);
				if( v != null)
				{
					reqSessionId = (String)v.firstElement();
					if( reqSessionId != null)
					{
						reqSessionIdFromURL = true;
					}
				}
			}

			// Specific SessionId requested
			if( reqSessionId != null)
			{
				// Look for session, but don't create
				session = getSession( reqSessionId);
				// If found
				if( session != null)
				{
					session.setLastAccessedTime();
					// Requested sessionId valid
					reqSessionIdValid = true;
				}
			}
		}
		catch( HttpException he)
		{
			throw he;
		}
		catch( InterruptedIOException iioe)
		{
			throw new HttpException( SC_REQUEST_TIMEOUT, RP_REQUEST_TIMEOUT);
		}
		catch( Exception e)
		{
			server.log( "Exception parsing request: ", e );
			throw new HttpException( SC_BAD_REQUEST, RP_BAD_REQUEST);
		}
	}

	protected void parsePostData()
	throws IOException
	{
		if( !postDataParsed && HttpMethods.POST.equals( getMethod() ))
		{
			postDataParsed = true;

			// Get content type and length
			String contentType = getHeader( CONTENT_TYPE);
			int contentLength = getIntHeader( CONTENT_LENGTH);
			if( FORM_URLENCODED.equalsIgnoreCase( contentType)
			&&  contentLength > 0 )
			// TODO Limit max length?
			{
				// Get form data
				byte[] content = new byte[contentLength];
				int totalBytesRead = 0;
				while( totalBytesRead < contentLength )
				{
					int bytesRead = is.read( content,
						totalBytesRead, contentLength - totalBytesRead);
					if( bytesRead == -1 ) throw new IOException( "Short read" );
					totalBytesRead += bytesRead;
				}
				String s = new String( content);
				StringBuffer sb = new StringBuffer( s);
				// Change '+' to ' '
				for( int i = 0; i < sb.length(); ++i)
				{
					if( sb.charAt( i) == '+')
						sb.setCharAt( i, ' ');
				}
				s = sb.toString();
				new NVPairParser( s, "&", "=")
				{
					public void forEachNVPair( String name, String value)
					{
						setParameter(
							httpDecode( name), httpDecode( value) );
					}
				}.parse();
			}
		}
	}

	protected final String readLine()
	throws IOException
	{
		StringBuffer sb = new StringBuffer();

		int ch = is.read();

		while( ch != '\r' && ch != '\n' && ch != -1)
		{
			sb.append( (char)ch);
			ch = is.read();
		}

		// If CR, handle CRLF
		if( ch == '\r')
		{
			is.mark( 1);
			ch = is.read();
			if( ch != '\n')
			{
				// put it back
				is.reset();
			}
		}

		return sb.toString();
	}

	protected
	void
	setHeader( String name, String value)
	{
		headers.put( name, value);
		// If cookies
		if( name.equalsIgnoreCase( "Cookie"))
		{
			setCookies( value);
		}
	}

	public
	Cookie
	getCookie( String name)
	{
		return (Cookie)cookies.get( httpDecode( name));
	}

	protected
	void
	setCookies( String cookieString)
	{
		new NVPairParser( cookieString, ";", "=")
		{
			public void forEachNVPair( String name, String value)
			{
				setCookie( name, value);
			}
		}.parse();
	}

	protected
	void
	setCookie( String name, String value)
	{
		cookies.put( httpDecode( name),
			new Cookie( httpDecode(name), httpDecode(value)) );
	}

	protected
	void
	pushParameter( String name, String value)
	{
		setParameter( name, value, true );
	}

	protected
	void
	popParameter( String name )
	{
		Vector v = (Vector)parameters.get( name);
		if( v != null )
		{
			v.removeElementAt( 0 );
			if( v.size() == 0 )
			{
				parameters.remove( name );
			}
		}
	}

	protected
	void
	setParameter( String name, String value)
	{
		setParameter( name, value, false );
	}

	protected
	void
	setParameter( String name, String value, boolean putInFront )
	{
		Vector v = (Vector)parameters.get( name);
		if( v == null)
		{
			v = new Vector();
			parameters.put( name, v);
		}
		if( putInFront )
		{
			v.insertElementAt( value, 0 );
		}
		else
		{
			v.addElement( value);
		}
	}

	protected
	void
	createSession()
	{
		session =
			new com.smartsc.http.HttpSession( server.getSessionTimeout());
		sessions.put( session.getId(), session);
	}

	protected
	com.smartsc.http.HttpSession
	getSession( String sessionId)
	{
		return (com.smartsc.http.HttpSession)sessions.get( sessionId);
	}

	protected
	void
	close()
	throws IOException
	{
		if( ir != null)
		{
			ir.close();
		}
		is.close();
	}

	protected static
	void
	removeSession( String id)
	{
		sessions.remove( id);
	}

	public static
	String
	httpDecode( String encoded)
	{
		if( encoded == null)
			return null;

		StringBuffer decoded = new StringBuffer();
		StringTokenizer st = new StringTokenizer( encoded, "%+", true);

		while( st.hasMoreTokens())
		{
			String token =  st.nextToken();
			if( "%".equals( token) && st.hasMoreTokens())
			{
				String nextChunk = st.nextToken();

				// If Unicode encoding
				if( nextChunk.charAt( 0 ) == 'u' )
				{
					// Find first non-digit in the next four characters
					int nonDigitIdx = 5;
					int max = Math.min( 5, nextChunk.length() );
					for( int i = 1; i < max; ++i )
					{
						char ch = nextChunk.charAt( i );

						if( (ch < '0' || '9' < ch)
						&&  (ch < 'a' || 'f' < ch)
						&&  (ch < 'A' || 'F' < ch) )
						{
							nonDigitIdx = i;
							break;
						}
					}
					// Make sure we have at least one digit
					if( nonDigitIdx == 1 )
					{
						throw new NumberFormatException(
							"Bad Unicode encoding: %" + nextChunk );
					}

					decoded.append( (char)
						Integer.parseInt( nextChunk.substring( 1, nonDigitIdx), 16));
					decoded.append( nextChunk.substring( nonDigitIdx));
				}
				else
				{
					decoded.append( (char)
						Integer.parseInt( nextChunk.substring( 0, 2), 16));
					decoded.append( nextChunk.substring( 2));
				}
			}
			else if( "+".equals( token))
			{
				decoded.append( " ");
			}
			else
			{
				decoded.append( token);
			}
		}

		return decoded.toString();
	}

	public static final String FORM_URLENCODED =
		"application/x-www-form-urlencoded";

	private HttpServer server;
	private Socket socket;

	private String method;

	private String verbatimRequestPath; // Actual text in HTTP request
	protected String getVerbatimRequestPath() { return verbatimRequestPath; }

	/**
	 * This is the complete request path (decoded) including any query string
	 * and fragment specifier.
	 */
	protected String requestPath;
	public String getRequestPath() { return requestPath; }
	/**
	 * This is the request URI portion of the request path (decoded).
	 * Does not contain any query string or fragment specifier.
	 */
	protected String requestURI;
	/**
	 * This is the part of the path the specifies which servlet to run.
	 */
	protected String[] servletPathAndName = new String[2];
	protected String pathInfo;
	protected String queryString;

	/**
	 * Parses the request path into its various elememnts.  The elements
	 * include <ol>
	 * <li>context path (always "" for TiniHttpServer)
	 * <li>servlet path (always starts with '/')
	 * <li>path info (rest of path that is not part of context or servlet path)
	 * <li>query string (everything following a '?' or null if not present)
	 * </ol>
	 */
	protected void parseRequestPath( String path )
	{
		// Decode path and save
		requestPath = httpDecode( path );

		// Create fragment-free local copy of request path
		int fragment = path.indexOf( '#' );
		if( fragment != -1 )
		{
			path = path.substring( 0, fragment );
		}
		requestURI = httpDecode( path );

		// Get path info and query string
		int queryIdx = requestURI.indexOf( '?' );
		if( queryIdx != -1 )
		{
			queryString = requestURI.substring( queryIdx + 1 );

			// Parse query string
			if( queryString.length() > 0 )
			{
				new NVPairParser( queryString, "&", "=")
				{
					public void forEachNVPair( String name, String value)
					{
						setParameter( httpDecode( name), httpDecode( value));
					}
				}.parse();
			}

			// Eliminate query string from requestURI
			requestURI = requestURI.substring( 0, queryIdx );
		}

		// Get servlet path portion of request path
		server.getServletPathAndName( requestURI, servletPathAndName );
		int servletPathLength = servletPathAndName[0].length();

		// Get path info portion of request path
		if( servletPathLength < requestURI.length() )
		{
			pathInfo = requestURI.substring( servletPathLength );
		}
	}

	private String protocol;

	private CaselessHashtable headers = new CaselessHashtable();
	private Hashtable cookies    = new Hashtable();
	private Hashtable parameters = new Hashtable();
	private Hashtable attributes = new Hashtable();
	private String    authType = null;
	private String    remoteUser = null;

	// Session variables
	static Hashtable sessions = new Hashtable();
	private com.smartsc.http.HttpSession session;
	private String sessionName;
	private String reqSessionId;
	private String sessionId;
	private boolean reqSessionIdFromCookie;
	private boolean reqSessionIdFromURL;
	private boolean reqSessionIdValid;

	private int nextChar = -1;
	private BufferedInputStream is;
	private boolean getISCalled = false;
	private BufferedReader ir = null;

	private boolean postDataParsed;

	// Create session reaper thread
	private static Thread reaperThread = new HttpSessionReaper();

	// From javax.servlet.ServletResponse
	public Object getAttribute( String name)
	{
		return attributes.get( name);
	}
	public Enumeration getAttributeNames()
	{
		return attributes.keys();
	}
	public String getCharacterEncoding()
	{
		// TODO
		return "";
	}
	public int getContentLength()
	{
		return getIntHeader( CONTENT_LENGTH);
	}
	public String getContentType()
	{
		return (String)headers.get( CONTENT_TYPE);
	}
	public ServletInputStream getInputStream()
	throws IOException
	{
		// If Reader has already been obtained
		if( ir != null)
		{
			throw new IllegalStateException(
				"getReader() was called first.");
		}
		getISCalled = true;
		return new com.smartsc.http.ServletInputStream( is);
	}
	public Locale getLocale()
	{
		return Locale.getDefault();
	}
	public Enumeration getLocales()
	{
		Vector v = new Vector( 1);
		v.addElement( Locale.getDefault());
		return v.elements();
	}
	public String getParameter( String name)
	{
		try { parsePostData(); }
		catch( IOException ioe ) { server.log( ioe ); }
		String param = null;
		Vector v = (Vector)parameters.get( name);
		if( v != null)
			param = (String)v.firstElement();
		return param;
	}
	public Enumeration getParameterNames()
	{
		try { parsePostData(); }
		catch( IOException ioe ) { server.log( ioe ); }
		return parameters.keys();
	}
	public String[] getParameterValues( String name)
	{
		try { parsePostData(); }
		catch( IOException ioe ) { server.log( ioe ); }
		Vector v = (Vector)parameters.get( name);
		if( v == null)
			return null;
		String[] values = new String[v.size()];
		v.copyInto( values);
		return values;
	}
	public String getProtocol()
	{
		return protocol;
	}
	public BufferedReader getReader()
	throws IOException
	{
		if( getISCalled)
		{
			throw new IllegalStateException(
				"getInputStream() was called first.");
		}
		if( ir == null)
		{
			ir = new BufferedReader(
				new InputStreamReader( is) );
		}
		return ir;
	}
	/** @deprecated */
	public String getRealPath( String path)
	{
		return server.getRealPath( path);
	}
	public String getRemoteAddr()
	{
		return socket.getInetAddress().getHostAddress();
	}
	public String getRemoteHost()
	{
		return socket.getInetAddress().getHostName();
	}
	public RequestDispatcher getRequestDispatcher( String urlPath)
	{
		// Sanity check
		if( urlPath == null) return null;

		// Resolve relative path
		if( !urlPath.startsWith( "/" ))
		{
			String uri = getRequestURI();
			int lastSlashIdx = uri.lastIndexOf( '/' );
			if( lastSlashIdx > 0 )
			{
				uri = uri.substring( 0, lastSlashIdx + 1 );
			}
			urlPath = uri + urlPath;
		}

		return server.getRequestDispatcher( urlPath );
	}
	public String getScheme()
	{
		// TODO Support https?
		return server.DEFAULT_SCHEME;
	}
	public String getServerName()
	{
		String serverName;
		String hostColonPort = getHeader( "Host");
		if( hostColonPort != null)
		{
			int colonIdx = hostColonPort.indexOf( ':');
			if( colonIdx != -1)
			{
				serverName = hostColonPort.substring( 0, colonIdx);
			}
			else
			{
				serverName = hostColonPort;
			}
		}
		else
		{
			serverName = socket.getLocalAddress().getHostName();
		}
		return serverName;
	}
	public int getServerPort()
	{
		int port = socket.getLocalPort();
		String hostColonPort = getHeader( "Host");
		if( hostColonPort != null)
		{
			int colonIdx = hostColonPort.indexOf( ':');
			if( colonIdx != -1)
			{
				try
				{
					port = Integer.parseInt(
						hostColonPort.substring( colonIdx + 1));
				}
				catch( NumberFormatException nfe) {}
			}
			else
			{
				port = HttpServer.DEFAULT_PORT;
			}
		}
		return port;
	}
	public boolean isSecure()
	{
		return false;
	}
	public void removeAttribute( String name)
	{
		attributes.remove( name);
	}
	public void setAttribute( String name, Object o)
	{
		attributes.put( name, o);
	}

	// From HttpServletRequest
	public String getAuthType()
	{
		return authType;
	}
	public String getContextPath()
	{
		return "";
	}
	public Cookie[] getCookies()
	{
		Cookie[] cookieArray = new Cookie[cookies.size()];
		Enumeration e = cookies.elements();
		for( int i = 0; e.hasMoreElements(); ++i)
		{
			cookieArray[i] = (Cookie)e.nextElement();
		}
		return cookieArray;
	}
	public long getDateHeader( String name)
	{
		long date = -1;
		String value = (String)headers.get( name);
		if( value != null)
		{
			try { date = Date.parse( value); }
			catch( IllegalArgumentException iae) {}
		}
		return date;
	}
	public String getHeader( String name)
	{
		return (String)headers.get( name);
	}
	public Enumeration getHeaders( String name)
	{
		return headers.getValues( name);
	}
	public Enumeration getHeaderNames()
	{
		return headers.keys();
	}
	public int getIntHeader( String name)
	{
		String value = (String)headers.get( name);
		if( value == null)
			return -1;
		else
			return Integer.parseInt( value);
	}
	public String getMethod()
	{
		return method;
	}
	public String getPathInfo()
	{
		return pathInfo;
	}
	public String getPathTranslated()
	{
		return server.getRealPath( getPathInfo());
	}
	public String getQueryString()
	{
		return queryString;
	}
	public String getRemoteUser()
	{
		return remoteUser;
	}
	public String getRequestURI()
	{
		return requestURI;
	}
	public String getRequestedSessionId()
	{
		return reqSessionId;
	}
	public String getServletPath()
	{
		return servletPathAndName[0];
	}
	protected String getServletName()
	{
		return servletPathAndName[1];
	}
	public HttpSession getSession()
	{
		return getSession( true);
	}
	public HttpSession getSession( boolean create)
	{
		if( session == null && create)
			createSession();

		return session;

		/*
		// If not creating new session
		// and requested session not valid
		if( !create && !reqSessionIdValid)
			return null;
		// Else, if we have a session
		// (regardless of create flag or whether it was valid as requested)
		else if( session != null)
			// Return requested session
			return session;
		// Else (Creating and requested session not valid)
		else // if( create and !reqSessionIdValid)
			// Return new session
			return createSession();
		*/
	}
	public java.security.Principal getUserPrincipal()
	{
		return null;
	}
	public boolean isRequestedSessionIdFromCookie()
	{
		return reqSessionIdFromCookie;
	}
	public boolean isRequestedSessionIdFromURL()
	{
		return reqSessionIdFromURL;
	}
	/** @deprecated */
	public boolean isRequestedSessionIdFromUrl()
	{
		return isRequestedSessionIdFromURL();
	}
	public boolean isRequestedSessionIdValid()
	{
		return reqSessionIdValid;
	}
	public boolean isUserInRole( String role)
	{
		return false;
	}
}
